package com.qsw.qswcommonlib.util;

import com.qsw.qswcommonlib.bean.GPSLocation;

/**
 * 地图坐标相关工具
 * <p>
 * 三种经纬度坐标系：
 * WGS-84：这是一个国际标准，也就是GPS坐标（Google Earth（不含中国）、或者GPS模块采集的都是这个类型）。
 * <p>
 * GCJ-02：中国坐标偏移标准,也称为火星坐标，它是在国际标准坐标WGS-84上进行的一次加密。
 * GCJ-02坐标是国家测绘局为了国家安全在原始坐标的基础上进行偏移得到的坐标，基本国内的电子地图、导航设备都是采用的这一坐标系或在这一坐标的基础上进行二次加密得到的。
 * 像是Google Map、高德、腾讯地图都是采用这种坐标展示。
 * <p>
 * BD-09：百度坐标偏移标准，百度地图专用的偏移标准，对GCJ-02进行二次加密得到的。
 */
public class CommonGPSUtil {
    /**
     * 地球半径6378.137，单位为千米
     */
    private static double EARTH_RADIUS = 6378.137;
    /**
     * 圆周率PI
     */
    private static double PI = Math.PI;
    /**
     * 卫星椭球坐标投影到平面地图坐标系的投影因子
     */
    private static double AXIS = 6378245.0;
    /**
     * 椭球的偏心率(a^2 - b^2) / a^2
     */
    private static double OFFSET = 0.00669342162296594323;
    /**
     * 圆周率转换量
     */
    private static double X_PI = PI * 3000.0 / 180.0;

    /**
     * 计算两个经纬度坐标之间的距离
     *
     * @param gpsLocation1 坐标1
     * @param gpsLocation2 坐标2
     * @return 距离，单位：米
     */
    public static double calLocationDistance(GPSLocation gpsLocation1, GPSLocation gpsLocation2) {
        return EARTH_RADIUS * Math.acos(
                Math.cos(Math.toRadians(gpsLocation1.getLatitude())) * Math.cos(Math.toRadians(gpsLocation2.getLatitude())) * Math.cos(Math.toRadians(gpsLocation1.getLongitude()) - Math.toRadians(gpsLocation2.getLongitude()))
                        + Math.sin(Math.toRadians(gpsLocation1.getLatitude())) * Math.sin(Math.toRadians(gpsLocation2.getLatitude()))
        ) * 1000;
    }

    private static final int THRESHOLDVALUE_CALLOCATIONBYDEGDIS_1 = 1000;// 根据距离计算坐标时的一级校准距离间隔
    private static final int THRESHOLDVALUE_CALLOCATIONBYDEGDIS_2 = 100;// 根据距离计算坐标时的二级校准距离间隔
    private static final int THRESHOLDVALUE_CALLOCATIONBYDEGDIS_3 = 10;// 根据距离计算坐标时的三级校准距离间隔

    /**
     * 根据初始经纬度和方向、距离计算新的经纬度信息
     *
     * @param location          起始点经纬度
     * @param radian            方位角，需要化为弧度，正北方为0，正西为90度，呈逆时针
     * @param distance          距离，单位：米
     * @param useVerifyDistance true：根据{@link #calLocationByDegDis(GPSLocation, double, double)}校准计算结果，直至推算距离接近distance。
     *                          校准依据为{@link #THRESHOLDVALUE_CALLOCATIONBYDEGDIS_1#THRESHOLDVALUE_CALLOCATIONBYDEGDIS_2#THRESHOLDVALUE_CALLOCATIONBYDEGDIS_3}
     * @return 新的经纬度信息
     */
    public static GPSLocation calLocationByDegDis(GPSLocation location, double radian, double distance, boolean useVerifyDistance) {
        GPSLocation resultLocation = calLocationByDegDis(location, radian, distance);
        if (useVerifyDistance) {
            double calDistance = distance;
            double verifyDistance = calLocationDistance(location, resultLocation);

            for (int i = 0; i < 3; i++) {
                double thresholdValue;// 阈值
                switch (i) {
                    case 0:
                        thresholdValue = THRESHOLDVALUE_CALLOCATIONBYDEGDIS_1;
                        break;
                    case 1:
                        thresholdValue = THRESHOLDVALUE_CALLOCATIONBYDEGDIS_2;
                        break;
                    case 2:
                        thresholdValue = THRESHOLDVALUE_CALLOCATIONBYDEGDIS_3;
                        break;
                    default:
                        thresholdValue = THRESHOLDVALUE_CALLOCATIONBYDEGDIS_3;
                        break;
                }
                boolean directionAdd = verifyDistance < distance;
                while (Math.abs(verifyDistance - distance) > thresholdValue) {
                    if (verifyDistance > distance) {
                        if (directionAdd) {
                            break;
                        }
                        calDistance -= thresholdValue;
                        if (calDistance <= 0) {
                            break;
                        }
                        resultLocation = calLocationByDegDis(location, radian, calDistance);
                    } else if (verifyDistance < distance) {
                        if (!directionAdd) {
                            break;
                        }
                        calDistance += thresholdValue;
                        if (calDistance <= 0) {
                            break;
                        }
                        resultLocation = calLocationByDegDis(location, radian, calDistance);
                    } else {
                        break;
                    }
                    verifyDistance = calLocationDistance(location, resultLocation);
                }
            }
        }
        return resultLocation;
    }

    /**
     * 根据初始经纬度和方向、距离计算新的经纬度信息
     *
     * @param location 起始点经纬度
     * @param radian   方位角，需要化为弧度，正北方为0，正西为90度，呈逆时针
     * @param distance 距离，单位：米
     * @return 新的经纬度信息
     */
    public static GPSLocation calLocationByDegDis(GPSLocation location, double radian, double distance) {
        double arc = EARTH_RADIUS * 1000;
        double longitude = location.getLongitude() + distance * Math.sin(radian) / (arc * Math.cos(location.getLatitude()) * 2 * Math.PI / 360);
        double latitude = location.getLatitude() + distance * Math.cos(radian) / (arc * 2 * Math.PI / 360);
        return new GPSLocation(longitude, latitude);
    }

    /**
     * GCJ-02坐标转换为BD-09坐标
     *
     * @param latitude  GCJ-02纬度
     * @param longitude GCJ-02经度
     * @return BD-09坐标
     */
    public static GPSLocation GCJ02ToBD09(double latitude, double longitude) {
        double x = longitude;
        double y = latitude;
        double z = Math.sqrt(x * x + y * y) + 0.00002 * Math.sin(y * X_PI);
        double theta = Math.atan2(y, x) + 0.000003 * Math.cos(x * X_PI);
        return new GPSLocation(z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006);
    }

    /**
     * BD-09坐标转换为GCJ-02坐标
     *
     * @param latitude  BD-09纬度
     * @param longitude BD-09经度
     * @return GCJ-02坐标
     */
    public static GPSLocation BD09ToGCJ02(double latitude, double longitude) {
        double x = longitude - 0.0065;
        double y = latitude - 0.006;
        double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
        double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
        return new GPSLocation(z * Math.cos(theta), z * Math.sin(theta));
    }

    /**
     * BD-09坐标转换为WGS84坐标
     *
     * @param latitude  BD-09纬度
     * @param longitude BD-09经度
     * @return WGS84坐标
     */
    public static GPSLocation BD09ToWGS84(double latitude, double longitude) {
        GPSLocation gpsLocation = BD09ToGCJ02(latitude, longitude);
        return GCJ02ToWGS84Rough(gpsLocation.getLatitude(), gpsLocation.getLongitude());
    }

    /**
     * WGS84坐标转换为BD-09坐标
     *
     * @param latitude  WGS84纬度
     * @param longitude WGS84经度
     * @return BD-09坐标
     */
    public static GPSLocation WGS84ToBD09(double latitude, double longitude) {
        GPSLocation gpsLocation = WGS84ToGCJ02(latitude, longitude);
        return GCJ02ToBD09(gpsLocation.getLatitude(), gpsLocation.getLongitude());
    }

    /**
     * WGS84坐标转换为GCJ02坐标
     *
     * @param latitude  WGS84纬度
     * @param longitude WGS84经度
     * @return GCJ02坐标
     */
    public static GPSLocation WGS84ToGCJ02(double latitude, double longitude) {
        GPSLocation gpsLocation = new GPSLocation();
        if (outOfChina(latitude, longitude)) {
            gpsLocation.setLatitude(latitude);
            gpsLocation.setLongitude(longitude);
            return gpsLocation;
        }
        double[] deltaD = transform(latitude, longitude);
        gpsLocation.setLatitude(latitude + deltaD[0]);
        gpsLocation.setLongitude(longitude + deltaD[1]);
        return gpsLocation;
    }

    /**
     * GCJ02坐标转换为WGS84坐标，粗略计算
     *
     * @param latitude  GCJ02纬度
     * @param longitude GCJ02经度
     * @return WGS84坐标
     */
    public static GPSLocation GCJ02ToWGS84Rough(double latitude, double longitude) {
        GPSLocation gpsLocation = new GPSLocation();
        if (outOfChina(latitude, longitude)) {
            gpsLocation.setLatitude(latitude);
            gpsLocation.setLongitude(longitude);
            return gpsLocation;
        }
        double[] deltaD = transform(latitude, longitude);
        gpsLocation.setLatitude(latitude - deltaD[0]);
        gpsLocation.setLongitude(longitude - deltaD[1]);
        return gpsLocation;
    }

    /**
     * GCJ02坐标转换为WGS84坐标，精确计算
     *
     * @param latitude  GCJ02纬度
     * @param longitude GCJ02经度
     * @return WGS84坐标
     */
    public static GPSLocation GCJ02ToWGS84Precise(double latitude, double longitude) {
        double initDelta = 0.01;
        double threshold = 0.000000001;
        double dLat = initDelta, dLon = initDelta;
        double mLat = latitude - dLat, mLon = longitude - dLon;
        double pLat = latitude + dLat, pLon = longitude + dLon;
        double wgsLat, wgsLon, i = 0;
        while (true) {
            wgsLat = (mLat + pLat) / 2;
            wgsLon = (mLon + pLon) / 2;
            GPSLocation gpsLocation = WGS84ToGCJ02(wgsLat, wgsLon);
            dLat = gpsLocation.getLatitude() - latitude;
            dLon = gpsLocation.getLongitude() - longitude;
            if ((Math.abs(dLat) < threshold) && (Math.abs(dLon) < threshold)) {
                break;
            }
            if (dLat > 0) {
                pLat = wgsLat;
            } else {
                mLat = wgsLat;
            }
            if (dLon > 0) {
                pLon = wgsLon;
            } else {
                mLon = wgsLon;
            }
            if (++i > 10000) {
                break;
            }
        }
        return new GPSLocation(wgsLon, wgsLat);
    }

    /**
     * 用于WGS-84与GCJ-02之间的坐标转换.
     *
     * @param latitude  维度
     * @param longitude 经度
     * @return double[] 两坐标系间的偏移
     */
    public static double[] transform(double latitude, double longitude) {
        double[] latlng = new double[2];
        double dLat = transformaLtitude(longitude - 105.0, latitude - 35.0);
        double dLon = transformLongitude(longitude - 105.0, latitude - 35.0);
        double radLat = latitude / 180.0 * PI;
        double magic = Math.sin(radLat);
        magic = 1 - OFFSET * magic * magic;
        double sqrtMagic = Math.sqrt(magic);
        dLat = (dLat * 180.0) / ((AXIS * (1 - OFFSET)) / (magic * sqrtMagic) * PI);
        dLon = (dLon * 180.0) / (AXIS / sqrtMagic * Math.cos(radLat) * PI);
        latlng[0] = dLat;
        latlng[1] = dLon;
        return latlng;
    }

    private static double transformaLtitude(double x, double y) {
        double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(y / 12.0 * PI) + 320 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
        return ret;
    }

    private static double transformLongitude(double x, double y) {
        double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
        ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
        return ret;
    }

    /**
     * 判断是否在国内，不在国内则不做偏移
     *
     * @param latitude  纬度
     * @param longitude 经度
     * @return true：在国外；false：在国内
     */
    private static boolean outOfChina(double latitude, double longitude) {
        // 纬度3.86~53.55,经度73.66~135.05
        return !(longitude > 73.66 && longitude < 135.05 && latitude > 3.86 && latitude < 53.55);
    }
}